AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Programmierung allgemein Netzwerke Delphi [Indy 10] TCP Server-Client: Sichere Kommunikation für ein Spiel
Thema durchsuchen
Ansicht
Themen-Optionen

[Indy 10] TCP Server-Client: Sichere Kommunikation für ein Spiel

Ein Thema von nuclearping · begonnen am 27. Sep 2012 · letzter Beitrag vom 29. Sep 2012
Antwort Antwort
nuclearping

Registriert seit: 7. Jun 2008
708 Beiträge
 
Delphi 10.2 Tokyo Professional
 
#1

[Indy 10] TCP Server-Client: Sichere Kommunikation für ein Spiel

  Alt 27. Sep 2012, 12:16
Hallo!

Ich bastel gerade am Netzwerkcode für mein kleines Spiel und nutze dafür Indy 10. Hab noch nie was mit Indy gemacht und in den letzten Tagen viel gelesen, viele Demos angeschaut (an dieser Stelle herzlichen Dank an bernhard_LA für seine Demos) und versucht, viel zu verstehen. Und so ganz grob vom Prinzip her funktioniert es schonmal, dass es eine Serveranwendung gibt, wo sich ein Game-Client problemlos verbinden kann (Betonung liegt auf ein). Problematisch wird's, wenn ein zweiter Client hinzukommt und damit die Kommunikation etwas komplexer wird.

Mir gehts nicht um das grundlegende theoretische Prinzip einer solchen Kommunikation, sondern um den technischen Aspekt, besonders in Zusammenhang mit Indy 10.

Zum Beispiel: Jeder Client stellt sich nach der Connection und dem Login beim Server vor, wie sich's für Gentlemen gehört: "Ich bin Charakter Müller, habe das Sprite A, besitze 100 HP und stehe an Position X, Y". Damit weiß der Server bescheid. Er prüft nun in regelmäßigen Abständen die Distanz zwischen den Clients.

Wenn ein Client in Sichtweite des anderen kommt, wird dieser erstmal in eine Liste aufgenommen, wenn er noch nicht drin ist, und dabei wird eine Nachricht an den sichtenden Client vorbereitet "Hey, hier kommt Client X in Sicht" und in eine Nachrichtenschleife gesteckt.

Das Problem, was diesem konkreten Fall deutlich wird: Sobald der 2. Client sich beim Server vorgestellt hat, wird er von der Sichtbarkeits-Routine erfasst, es wird eine Nachricht erstellt und diese wird sofort gesendet.

Der 2. GameClient erhält nun diese "Hey, hier kommt Client X in Sicht" noch bevor er überhaupt mit der Anmeldung richtig durch war, was ihn gegen die Wand fahren lässt, weil er damit nicht rechnet.

Wie oben schon gesagt: Was hier deutlich wird und auch an anderen Stellen auftreten kann, dass sich Nachrichten überlappen können. Dass der Server eine Mitteilung zu machen hat und damit eine laufende Kommunikation zw. Client und Server unterbricht, bzw. stört.

Wie kann ich das verhindern? Der Server darf nichts rausschicken, solange wie eine Clientanfrage noch nicht beantwortet ist.

Mit "Prüfvariablen" kann ich scheinbar wegen der Threadsicherheit nicht arbeiten. Zumindest habe ich das schon versucht und es hat nicht geklappt. Jeder Klient hat eine Variable "IsWaitingForResponse" bekommen. Die wurde am Anfang von "OnExecute" auf TRUE gesetzt und am Ende wieder auf FALSE. In der Nachrichtenschleife wurde diese dann geprüft, mit der Feststellung im Debugger, dass sie irgendwie ständig auf TRUE steht, obwohl keine Kommunikation stattfand ...

Message-Handler
Delphi-Quellcode:
unit UMessageHandler;

interface

uses
  Classes, IdTask, IdContext, IdTCPServer, IdSchedulerOfThreadDefault,
  IdYarn, IdThread,

  UServerTypes;

type
  TMessageItem = record
    Id: Integer;
    Delete: Boolean;
    Context: TIdContext;
    Data: TNetworkDataRecord;
  end;
  PMessageItem = ^TMessageItem;

  TMessageHandler = class(TIdThread)
    constructor Create;
    destructor Destroy; override;
  protected
    FMessageList: TList;

    procedure Run; override;
  public
    function AddMessage(AMessage: TMessageItem): Integer;
    procedure RemoveMessage(Id: Integer);
  end;

var
  MessageHandler: TMessageHandler;

implementation

uses
  SysUtils,
  Windows,
  Forms,

  IdSync,

  UEventLogHandler,
  UServerClasses,
  UServerFunctions;

procedure ClearMessageList(AList: TList);
var
  i: Integer;
  Item: PMessageItem;
begin
  for i := 0 to AList.Count - 1 do
    begin
      Item := PMessageItem(AList[i]);
      Dispose(Item);
    end;
  AList.Clear;
end;

{ TMessageHandler }

function TMessageHandler.AddMessage(AMessage: TMessageItem): Integer;
var
  Item: PMessageItem;
begin
  New(Item);
  Item^ := AMessage;
  Item^.Delete := FALSE;
  Item^.Id := FMessageList.Add(Item);
  Result := Item^.Id;
end;

constructor TMessageHandler.Create;
begin
  inherited Create;

  FMessageList := TList.Create;
end;

destructor TMessageHandler.Destroy;
begin
  ClearMessageList(FMessageList);
  FreeAndNil(FMessageList);
  inherited;
end;

procedure TMessageHandler.Run;
var
  Buffer: TBytes;
  MsgIndex: Integer;
  Item: PMessageItem;
  Client: TClientContext;
begin
  MsgIndex := 0;
  while not Terminated do
    begin
      if MsgIndex > FMessageList.Count then
        MsgIndex := 0;

      if (FMessageList.Count = 0) then
        begin
          Sleep(1);
          Continue;
        end;

      Item := PMessageItem(FMessageList[MsgIndex]);
      if Item^.Delete then
        begin
          RemoveMessage(Item^.Id);
          MsgIndex := 0;
          Continue;
        end;

      Client := TClientContext(Item^.Context.Data);
      if not Assigned(Client) then
        Continue;

      Buffer := DataRecordToByteArray(Item^.Data);
      try
        if (Item^.Context.Connection.IOHandler.Connected) and not
          Client.WaitingForResponse then // Client.WaitingForResponse ergab immer TRUE, obwohl es keinen Sinn machte
          begin
            if not SendBuffer(Item^.Context, Buffer) then
              TIdNotify.NotifyMethod(EventLogHandler.SendBufferError)
            else
              Item^.Delete := TRUE;
          end;
      finally
        Finalize(Buffer);
      end;

      //Inc(MsgIndex);
    end;
end;

procedure TMessageHandler.RemoveMessage(Id: Integer);
var
  i: Integer;
  Item: PMessageItem;
begin
  for i := 0 to FMessageList.Count - 1 do
    begin
      Item := PMessageItem(FMessageList[i]);
      if Item^.Id = Id then
        begin
          FMessageList.Delete(i);
          Dispose(Item);
          Exit;
        end;
    end;
end;

end.
OnConnect/OnDisconnect/OnExecute-Behandlung
Delphi-Quellcode:
procedure TMyGameServer.OnClientConnect(AContext: TIdContext);
var
  Context: TClientContext;
begin
  FStatus := ssClientConnected;
  Context := TClientContext.Create(AContext);
  Context.ClientsList := FClientsList;
  AContext.Data := Context;
  AddClient(AContext);
end;

procedure TMyGameServer.OnClientDisconnect(AContext: TIdContext);
begin
  FStatus := ssClientDisconnected;
  if Assigned(AContext.Data) then
    begin
      if AContext.Data is TClientContext then
        begin
          RemoveClient(AContext);
          TClientContext(AContext.Data).Free;
        end;
      AContext.Data := nil;
    end;
end;

procedure TMyGameServer.OnServerExecute(AContext: TIdContext);
var
  ClientContext: TClientContext;
begin
  AContext.Connection.IOHandler.ReadTimeout := 10000;
  FStatus := ssProcessingData;
  try
    ClientContext := TClientContext(AContext.Data);
  except
    ClientContext := nil;
  end;

  if Assigned(ClientContext) then
    try
      if not ClientContext.ProcessData(AContext) then
        begin
          FStatus := ssError;
          TIdNotify.NotifyMethod(EventLogHandler.GetBufferError);
        end;
      FStatus := ssIdle;
    except
      FStatus := ssError;
      TIdNotify.NotifyMethod(EventLogHandler.ProcessError);
    end;
end;
TClientContext.ProcessData
Delphi-Quellcode:
unit UServerClasses;

interface

...

type
  TClientContext = class(TIdThreadSafe)
    ...
  public
    function ProcessData(AContext: TIdContext): Boolean; overload;
  end;

implementation

...

function TClientContext.ProcessData(AContext: TIdContext): Boolean;
var
  Buffer: TBytes;
begin
  FIsWaitingForResponse := FALSE; // TRUE ergibt, dass der Wert bei Prüfung in Nachrichtenschleife immer auf TRUE steht, obwohl er unten wieder auf FALSE gesetzt wird :(
  Result := FALSE;
  try
    if not ReceiveBuffer(AContext, Buffer) then
      Exit;

    if Length(Buffer) = 0 then
      Exit;

    Lock;
    try
      try
        FData := ByteArrayToDataRecord(Buffer);
      finally
        Finalize(Buffer);
      end;

      ProcessData;
    finally
      Unlock;
    end;

    Buffer := DataRecordToByteArray(ResponseData);
    try
      if not SendBuffer(AContext, Buffer) then
        Exit;
    finally
      Finalize(Buffer);
    end;

    Result := TRUE;
  finally
    FIsWaitingForResponse := FALSE;
  end;
end;

procedure TClientContext.UpdateClients;
var
  i: Integer;
  Context: TIdContext;
  ClientContext: TClientContext;
  DistanceBetween: Double;
begin
  for i := 0 to FClientsList.Count - 1 do
    begin
      if not FCanUpdateClients then
        Exit;

      Context := TIdContext(FClientsList[i]);
      if Context = FContext then
        Continue;

      ClientContext := (Context.Data as TClientContext);
      DistanceBetween := GetDistance(
        FCharacterPosition.X, FCharacterPosition.Y,
        ClientContext.PositionData.X, ClientContext.PositionData.Y);
      // Kommt ein Character in den Sichtbereich?
      if DistanceBetween <= CLIENT_VISIBILITY_DISTANCE then
        begin
          if not HasNearClient(Context) and IsValidClient(ClientContext) then
            AddNearClient(Context);
        end
      else
        begin
          // Verlässt ein sichtbarer Character den Sichtbereich?
          if HasNearClient(Context) then
            begin
              if DistanceBetween > CLIENT_VISIBILITY_DISTANCE then
                RemoveNearClient(Context);
            end;
        end;
    end;
end;

procedure TClientContext.AddNearClient(AContext: TIdContext);
begin
  FNearClientsList.Add(AContext);
  HandleNearClient(clAppear, AContext);
end;

procedure TClientContext.HandleNearClient(ACommand: TCommandsList;
  AContext: TIdContext);
var
  Data: TNetworkDataRecord;
  CharacterData: TIntroductionRecord;
  Buffer: TBytes;
  i: Integer;
begin
  // ...

  Broadcast(Data, FNearClientsList);
end;

procedure TClientContext.Broadcast(AData: TNetworkDataRecord; AClientsList: TList);
var
  i: Integer;
  Msg: TMessageItem;
  Context: TIdContext;
  List: TList;
begin
  List := AClientsList;
  for i := 0 to List.Count - 1 do
    begin
      Context := TIdContext(List[i]);
      if Context = FContext then
        Continue;
      Msg.Context := Context;
      Msg.Data := AData;
      MessageHandler.AddMessage(Msg);
    end;
end;

end.
Ich hoffe, es ist nicht zu viel und nicht zu wenig Code und dass ihr durchblickt.

Bin für jede Anregung und Hilfe dankbar.
  Mit Zitat antworten Zitat
Benutzerbild von BUG
BUG

Registriert seit: 4. Dez 2003
Ort: Cottbus
2.094 Beiträge
 
#2

AW: [Indy 10] TCP Server-Client: Sichere Kommunikation für ein Spiel

  Alt 27. Sep 2012, 13:10
Hi,

es ist vermutlich besser, das Protokoll erstmal ohne eine Implementierung zu entwerfen.

Ein ordentliches Zustandsdiagramm zwingt dich, das Protokoll richtig zu durchdenken, Abläufe kannst du dir mit (Überraschung) einem Ablaufdiagramm visualisieren.
Wenn du Sender und Empfänger als Zustandsautomaten modellierst, wird die Implementation nachher sehr viel einfacher, als wenn du einfach drauf los codest.

Dein Problem ist nicht wirklich die konkrete Implementation, sondern das Konzept:
Das Problem, was diesem konkreten Fall deutlich wird: Sobald der 2. Client sich beim Server vorgestellt hat, wird er von der Sichtbarkeits-Routine erfasst, es wird eine Nachricht erstellt und diese wird sofort gesendet.

Der 2. GameClient erhält nun diese "Hey, hier kommt Client X in Sicht" noch bevor er überhaupt mit der Anmeldung richtig durch war, was ihn gegen die Wand fahren lässt, weil er damit nicht rechnet.
Mit einem genauen Entwurf und dem Einsatz von TCP als drunterliegendes Protokoll weißt du fast immer genau, in welchem Zustand sich die Gegenstelle befindet.

Dein konkretes Problem würde eine Art Kanäle/Ports lösen. Beim Lesen der Nachrichten kann festgelegt werden, aus welchem Kanal gelesen wird (Authentifizierung/Broadcast/usw.). Alle anderen Nachrichten bleiben in einer Warteschlange. Das könnte aber sehr unübersichtlich werden.
Intellekt ist das Verstehen von Wissen. Verstehen ist der wahre Pfad zu Einsicht. Einsicht ist der Schlüssel zu allem.
  Mit Zitat antworten Zitat
nuclearping

Registriert seit: 7. Jun 2008
708 Beiträge
 
Delphi 10.2 Tokyo Professional
 
#3

AW: [Indy 10] TCP Server-Client: Sichere Kommunikation für ein Spiel

  Alt 28. Sep 2012, 07:58
Danke für die Tipps.

Ich bin zugegebenermaßen auch nicht so der Theoretiker. Ich kann minutenlang vor einer Mindmap oder einem Ablaufplan sitzen, ohne dass da viel in meinem Kopf passiert, aber wenn ich 'ne IDE vor mir hab und "drauflos programmieren" oder mich mit Leuten darüber austauschen kann, kommen die Ideen, Zusammenhänge und Abläufe automatisch.
Aber das verläuft nicht so chaotisch, wie's vielleicht klingen mag. Ich achte da schon immer auf ordentliches objekt-orientiertes Design, dass ich generische Klassen erstelle und diese sinnvoll vererbe und erweitere. Und viel mehr als ein paar Sachen umstrukturieren oder neu verschachteln muss ich da inzwischen auch nicht mehr.

Das Protokoll ist mir im Kopf soweit eigentlich schon klar:

Jede aktive oder passive Zustandsänderung seitens des Clients wird an den Server gemeldet, der wiederum an alle relevanten (in der Nähe befindlichen) Clients ein Statusupdate des jeweiligen Characters sendet und sich mit diesen synchronisiert. Also wenn ein Charakter anfängt zu laufen, wird ein STARTWALKING an den Server geschickt, der schickt an die anderen Clients nun das Update STARTWALKING CharacterID, ebenso mit STOPWALKING oder wenn der Character sich dreht, anfängt zu zaubern, den Bogen spannt, Schaden von einer anderen Quelle bekommt, sich heilt, usw. Danach folgt ein SYNCPOSITION, SYNCSTATUS, SYNCirgendwas, um Lags / Desyncs zu korrigieren.

Es scheitert eben "nur" an der technischen Hürde und meiner Unerfahrenheit mit Indy bzw. generell des für mich neuen Sektors der Netzwerkkommunikation. Und ich denke auf technische Hürden trifft man auch, wenn das Protokoll vorher theoretisch klar ist, man aber noch nie mit der entsprechenden Schnittstelle gerarbeitet hat. Oder?

Aber der Tipp mit verschiedenen Kanälen hat mir geholfen. Hab mich diesbezüglich mal belesen und ein paar gut verständliche Beispiele gefunden, zB http://www.tek-tips.com/faqs.cfm?fid=7566
  Mit Zitat antworten Zitat
Benutzerbild von Aphton
Aphton

Registriert seit: 31. Mai 2009
1.198 Beiträge
 
Turbo Delphi für Win32
 
#4

AW: [Indy 10] TCP Server-Client: Sichere Kommunikation für ein Spiel

  Alt 28. Sep 2012, 08:43
Bist du dir sicher, dass "TMessageHandler.Run;"
überschrieben werden muss, anstelle "TMessageHandler.Execute"?
das Erkennen beginnt, wenn der Erkennende vom zu Erkennenden Abstand nimmt
MfG

Geändert von Aphton (28. Sep 2012 um 08:50 Uhr)
  Mit Zitat antworten Zitat
nuclearping

Registriert seit: 7. Jun 2008
708 Beiträge
 
Delphi 10.2 Tokyo Professional
 
#5

AW: [Indy 10] TCP Server-Client: Sichere Kommunikation für ein Spiel

  Alt 29. Sep 2012, 17:54
Ja, nachdem ich durch das "Multiple Ports"-Beispiel durchgestiegen bin, musste ich feststellen, dass ich das Pferd hier ohnehin von hinten aufgezäumt habe.

Hab den ganzen Code inzwischen verworfen und an Hand dieses Beispiels das nun so neu geschrieben, dass es erstens Indy- und Thread-Konform ist, zweitens Sinn macht und drittens auch funktioniert. Hab's noch nicht in's Spiel integriert, erstmal nur 'ne Anwendung für "Trockenübungen" und Stresstests. Läuft gut.
  Mit Zitat antworten Zitat
Antwort Antwort


Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 08:01 Uhr.
Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz